using System.Linq.Expressions;

namespace StudentVideo.Helper;

/// <summary>
/// 查询操作符枚举
/// </summary>
public enum QueryOperator {
   /// <summary>
   /// 相等
   /// </summary>
   Equals,
   /// <summary>
   /// 不等
   /// </summary>
   NotEquals,
   /// <summary>
   /// 包含
   /// </summary>
   Contains,
   StartsWith,
   EndsWith,
   GreaterThan,
   GreaterThanOrEqual,
   LessThan,
   LessThanOrEqual,
   In,
   Between
}

/// <summary>
/// 条件组合类型
/// </summary>
public enum ConditionCombineType {
   And,
   Or
}

/// <summary>
/// 查询映射配置帮助类
/// </summary>
public class QueryMapping<TEntity, TDto> where TEntity : class {
   private readonly Dictionary<string, List<PropertyMapping>> _mappings = [];

   public class PropertyMapping {
      public required Expression<Func<TEntity, object>> PropertySelector { get; set; }
      public QueryOperator Operator { get; set; }
      public bool IgnoreNull { get; set; } = true;
   }

   /// <summary>
   /// 实例化 QueryBuilder
   /// </summary>
   /// <returns></returns>
   public QueryBuilder<TEntity, TDto> QueryBuilder() {
      return new QueryBuilder<TEntity, TDto>(this);
   }

   /// <summary>
   /// 实例化 QueryBuilder 并构建查询
   /// </summary>
   /// <param name="source"></param>
   /// <param name="dto"></param>
   /// <returns></returns>
   public IQueryable<TEntity> BuildQuery(IQueryable<TEntity> source, TDto dto) {
      return new QueryBuilder<TEntity, TDto>(this).BuildQuery(source, dto);
   }

   /// <summary>
   /// 添加映射
   /// </summary>
   /// <param name="dtoProperty"></param>
   /// <param name="entityProperty"></param>
   /// <param name="op"></param>
   /// <param name="ignoreNull"></param>
   /// <returns></returns>
   public QueryMapping<TEntity, TDto> AddMapping(
      Expression<Func<TDto, object?>> dtoProperty,
      Expression<Func<TEntity, object?>> entityProperty,
      QueryOperator op = QueryOperator.Equals,
      bool ignoreNull = true
   ) {
      var dtoPropertyName = GetPropertyName(dtoProperty);

      if (!_mappings.TryGetValue(dtoPropertyName, out var value)) {
         value = [];
         _mappings[dtoPropertyName] = value;
      }

      value.Add(new PropertyMapping {
         PropertySelector = entityProperty!,
         Operator = op,
         IgnoreNull = ignoreNull
      });

      return this;
   }

   /// <summary>
   /// 批量添加同一DTO属性到多个实体属性的映射（默认使用Contains操作符和OR组合）
   /// </summary>
   public QueryMapping<TEntity, TDto> AddMapping(Expression<Func<TDto, object?>> dtoProperty, params Expression<Func<TEntity, object?>>[] entityProperties) {

      foreach (var entityProperty in entityProperties) {
         AddMapping(dtoProperty, entityProperty, QueryOperator.Contains);
      }

      return this;
   }

   public IEnumerable<PropertyMapping> GetMappings(string dtoPropertyName) {
      return _mappings.TryGetValue(dtoPropertyName, out var mappings)
          ? mappings
          : Enumerable.Empty<PropertyMapping>();
   }

   private static string GetPropertyName<T>(Expression<Func<T, object?>> expression) {
      if (expression.Body is MemberExpression memberExpression) {
         return GetFullPath(memberExpression);
      }

      if (expression.Body is UnaryExpression unaryExpression &&
          unaryExpression.Operand is MemberExpression unaryMemberExpression) {
         return GetFullPath(unaryMemberExpression);
      }

      throw new ArgumentException("Invalid property expression");
   }

   private static string GetFullPath(MemberExpression? expression) {
      var parts = new List<string>();
      while (expression != null) {
         parts.Insert(0, expression.Member.Name);
         expression = expression.Expression as MemberExpression;
      }
      return string.Join(".", parts);
   }
}

/// <summary>
/// 查询构建器
/// </summary>
public class QueryBuilder<TEntity, TDto>(QueryMapping<TEntity, TDto> mapping) where TEntity : class {
   private readonly QueryMapping<TEntity, TDto> _mapping = mapping ?? throw new ArgumentNullException(nameof(mapping));

   public IQueryable<TEntity> BuildQuery(IQueryable<TEntity> source, TDto dto) {
      if (dto == null)
         return source;

      var parameter = Expression.Parameter(typeof(TEntity), "x");
      Expression? combinedExpression = null;

      foreach (var prop in typeof(TDto).GetProperties()) {
         var value = prop.GetValue(dto);
         var mappings = _mapping.GetMappings(prop.Name);

         foreach (var mapping in mappings) {
            if (mapping.IgnoreNull && value == null)
               continue;

            var entityExpr = GetNestedPropertyExpression(mapping.PropertySelector, parameter);
            var condition = BuildCondition(entityExpr, value, mapping.Operator);

            combinedExpression = combinedExpression == null
                ? condition
                : Expression.AndAlso(combinedExpression, condition);
         }
      }

      var expression = combinedExpression == null
          ? null
          : Expression.Lambda<Func<TEntity, bool>>(combinedExpression, parameter);

      if (expression != null) {
         return source.Where(expression);
      }

      return source;
   }

   private Expression GetNestedPropertyExpression(
      Expression<Func<TEntity, object>> propertySelector,
      ParameterExpression parameter
   ) {
      Expression memberExpression = parameter;

      if (propertySelector.Body is MemberExpression body) {
         var propertyNames = GetPropertyPath(body);
         foreach (var name in propertyNames) {
            memberExpression = Expression.PropertyOrField(memberExpression, name);
         }
         return memberExpression;
      }

      if (propertySelector.Body is UnaryExpression unaryExpression &&
          unaryExpression.Operand is MemberExpression unaryMemberExpression) {
         var propertyNames = GetPropertyPath(unaryMemberExpression);
         foreach (var name in propertyNames) {
            memberExpression = Expression.PropertyOrField(memberExpression, name);
         }
         return memberExpression;
      }

      throw new ArgumentException("Invalid property selector expression");
   }

   private static List<string> GetPropertyPath(MemberExpression? expression) {
      var parts = new List<string>();
      while (expression != null) {
         parts.Insert(0, expression.Member.Name);
         expression = expression.Expression as MemberExpression;
      }
      return parts;
   }

   private static Expression BuildCondition(Expression property, object? value, QueryOperator op) {
      var constant = Expression.Constant(value);
      var convertedValue = Expression.Convert(constant, property.Type);

      return op switch {
         QueryOperator.Equals => Expression.Equal(property, convertedValue),
         QueryOperator.NotEquals => Expression.NotEqual(property, convertedValue),
         QueryOperator.Contains => Expression.Call(property, typeof(string).GetMethod("Contains", [typeof(string)])!, constant),
         QueryOperator.StartsWith => Expression.Call(property, typeof(string).GetMethod("StartsWith", [typeof(string)])!, constant),
         QueryOperator.EndsWith => Expression.Call(property, typeof(string).GetMethod("EndsWith", [typeof(string)])!, constant),
         QueryOperator.GreaterThan => Expression.GreaterThan(property, convertedValue),
         QueryOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(property, convertedValue),
         QueryOperator.LessThan => Expression.LessThan(property, convertedValue),
         QueryOperator.LessThanOrEqual => Expression.LessThanOrEqual(property, convertedValue),
         QueryOperator.In => BuildInExpression(property, value as IEnumerable<object>),
         QueryOperator.Between => BuildBetweenExpression(property, value),
         _ => throw new ArgumentOutOfRangeException(nameof(op), op, null)
      };
   }

   private static Expression BuildInExpression(Expression property, IEnumerable<object>? values) {
      if (values == null || !values.Any())
         return Expression.Constant(false);

      var containsMethod = typeof(Enumerable).GetMethods()
          .First(m => m.Name == "Contains" && m.GetParameters().Length == 2)
          .MakeGenericMethod(property.Type);

      var constant = Expression.Constant(values);
      return Expression.Call(containsMethod, constant, property);
   }

   private static Expression BuildBetweenExpression(Expression property, object? value) {
      if (value is not IEnumerable<object> range || range.Count() != 2)
         throw new ArgumentException("Between操作需要提供 [min, max] 数组");

      var min = Expression.Constant(range.First(), property.Type);
      var max = Expression.Constant(range.Last(), property.Type);

      return Expression.AndAlso(
          Expression.GreaterThanOrEqual(property, min),
          Expression.LessThanOrEqual(property, max)
      );
   }
}
